﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Pipes.Queries.Parser;

namespace Pipes.Queries.Model.Query
{
  public class Function : IExpression
  {
    public enum StdFunction
    {
      Unknown,
      Abs,
      Acos,
      Asin,
      Atan,
      Atan2,
      Ceiling,
      Conv,
      Cos,
      Cot,
      Crc32,
      Degrees,
      Exp,
      Floor,
      Log,
      Pi,
      Radians,
      Rand,
      Round,
      Sign,
      Sin,
      Sqrt,
      Tan,
      Truncate
    }

    private List<IExpression> _args;

    public StdFunction Standard { get; set; }
    public bool Deterministic 
    { 
      get 
      {
        if (this.Standard != StdFunction.Unknown)
        {
          foreach (var arg in _args)
          {
            if (!arg.Deterministic) return false;
          }
          return true;
        }
        return false;
      } 
    }
    public Pipes.Queries.MetaData.GeneralDataType InferredType { get; set; }
    public Identifier Name { get; set; }
    public ResultScope Scope { get; set; }
    public IList<IExpression> Args
    {
      get
      {
        return _args;
      }
    }

    public Function() 
    {
      this.Standard = StdFunction.Unknown;
      _args = new List<IExpression>();
    }
    public Function(IEnumerable<IExpression> args) 
    {
      this.Standard = StdFunction.Unknown;
      _args = new List<IExpression>(args);
    }

    public override string ToString()
    {
      var builder = new StringBuilder(this.Name.ToString());
      builder.Append('(');
      for (var i = 0; i < this.Args.Count; i++) 
      {
        if (i > 0) builder.Append(", ");
        builder.Append(this.Args[i].ToString());
      }
      builder.Append(')');

      return builder.ToString();
    }

    public object Evaluate()
    {
      if (this.Standard == StdFunction.Unknown)
      {
        throw new NotSupportedException();
      }
      else
      {
        return Execution.FunctionExecution.Execute(this.Standard, this.Args);
      }
    }

    public static IExpression GetExpression(ResultScope scope, Identifier name, IList<IExpression> args)
    {
      Function func;

      if (scope == ResultScope.All) {
        switch (name.ToString('\0', '.').ToLowerInvariant())
        {
          // Mathematical Functions
          // Standard form for a logarithm is going to be log(num, base) even though Oracle and MySql have this reversed
          case "ln":
            if (args.Count == 1)
            {
              return new Function(args) { Scope = scope, Name = new Identifier("log"), Standard = StdFunction.Log };
            }
            break;
          case "log10":
            if (args.Count == 1)
            {
              func = new Function(args) { Scope = scope, Name = new Identifier("log"), Standard = StdFunction.Log };
              func.Args.Add(new NumericLiteral(10));
              return func;
            }
            break;
          case "log2":
            if (args.Count == 1)
            {
              func = new Function(args) { Scope = scope, Name = new Identifier("log"), Standard = StdFunction.Log };
              func.Args.Add(new NumericLiteral(2));
              return func;
            }
            break;
          // Atan2 should follow the y, x format (since most math programs use this format: maxima, scilab, etc.)
          // However Atn2 is distinct to SQL server and will follow the x, y format
          case "atan":
            if (args.Count == 2)
            {
              return new Function(args) { Scope = scope, Name = new Identifier("atan2"), Standard = StdFunction.Atan2 };
            }
            break;
          case "atn2":
            if (args.Count == 2)
            {
              return new Function(args.Reverse()) { Scope = scope, Name = new Identifier("atan2"), Standard = StdFunction.Atan2 };
            }
            break;
          case "ceil":
            if (args.Count == 1)
            {
              return new Function(args) { Scope = scope, Name = new Identifier("ceiling"), Standard = StdFunction.Ceiling };
            }
            break;
          case "mod":
            if (args.Count == 2)
            {
              return new Operation(args) { Type = Operator.Modulus };
            }
            break;
          case "pow":
          case "power":
            if (args.Count == 2)
            {
              return new Operation(args) { Type = Operator.Power };
            }
            break;
          case "sqr":
            if (args.Count == 1)
            {
              return new Function(args) { Scope = scope, Name = new Identifier("sqrt"), Standard = StdFunction.Sqrt };
            }
            break;
          case "sqrtpi":
            if (args.Count == 1)
            {
              var op = new Operation(args) { Type = Operator.Multiplication };
              op.Args.Add(new Function() { Scope = scope, Name = new Identifier("pi") });
              func = new Function() { Scope = scope, Name = new Identifier("sqrt"), Standard = StdFunction.Sqrt };
              func.Args.Add(op);
              return func;
            }
            break;
          case "square":
            if (args.Count == 1)
            {
              var op = new Operation(args) { Type = Operator.Power };
              op.Args.Add(new NumericLiteral(2));
              return op;
            }
            break;

          // Conditional Functions
          case "if":
          case "iif":
            if (args.Count == 3)
            {
              var cond = new Conditional() { Else = args[2] };
              cond.Branches.Add(new ConditionBranch { Branch = args[1], Condition = args[0] });
              return cond;
            }
            break;
          case "choose":
            if (args.Count >= 2)
            {
              var cond = new Conditional() { Else = new Constant(ConstantType.@null), Root = args[0] };
              for (var i = 1; i < args.Count; i++)
              {
                cond.Branches.Add(new ConditionBranch { Branch = args[i], Condition = new NumericLiteral(i) });
              }
              return cond;
            }
            break;
          case "decode":
            if (args.Count >= 3)
            {
              var cond = new Conditional() { Root = args[0] };
              if (args.Count % 2 == 0)
              {
                cond.Else = args[args.Count - 1];
              }
              else
              {
                cond.Else = new Constant(ConstantType.@null);
              }
              for (var i = 1; i < args.Count; i += 2)
              {
                cond.Branches.Add(new ConditionBranch { Branch = args[i+1], Condition = args[i] });
              }
              return cond;
            }
            break;

          // Comparison Functions
          case "like":  // From sqlite
            if (args.Count == 2 || args.Count == 3)
            {
              //Need to implement
            }
            break;
          case "regexp_like":
            if (args.Count >= 2)
            {
              var op = new Operation() { Type = Operator.PatternMatch };
              op.Args.Add(args[0]);
              var str = args[1] as StringLiteral;

              var parser = new RegExpParser();
              using (var reader = new System.IO.StringReader(str.Value))
              {
                if (parser.Parse(reader))
                {
                  op.Args.Add(parser.PatternOpts);
                }
              }
              return op;
            }
            break;
        }
      }

      return new Function(args) { Scope = scope, Name = name };
    }

    public void Visit(Parser.IQueryVisitor visitor)
    {
      visitor.Visit(this);
    }
  }
}
